﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Forms;
using System.IO;
using WPFEdit.Document.Factory;
using System.Reflection;
using WPFEdit.Framework;
using WPFEdit.Document.ViewModels;
using System.Collections.ObjectModel;
using System.Windows.Input;
using WPFEdit.ViewModels;
using log4net;
using WPFEdit.Document;

namespace WPFEdit.Document
{
	/// <summary>
	/// Provides methods for opening and unmanaged and managed files. Managed files
	/// will be displayed in a tab view if the TabControlContext is used as a data context
	/// for a Tab View control.
	/// </summary>
    public class DocumentManager
    {
        public class TabControlContext
        {
            private ObservableCollection<OpenDocumentInfo> m_tabs;

            public ObservableCollection<OpenDocumentInfo> TabItems
            {
                get { return m_tabs; }
                internal set { m_tabs = value; }
            }
        }

		private class DocumentTask : Task
		{
			public Document WorkingDocument;
		}

        protected DocReaderFactory m_docReaderFactory = new DocReaderFactory();
        protected DocWriterFactory m_docWriterFactory = new DocWriterFactory();
        protected DocEditorFactory m_docEditorFactory = new DocEditorFactory();

        protected ObservableCollection<OpenDocumentInfo> m_documents = new ObservableCollection<OpenDocumentInfo>();
        protected TabControlContext m_tabViewContext;

        protected ICommand m_closeTabCommand;

		protected TaskManager m_taskManager = new TaskManager();

        private OpenDocumentInfo m_selectedDocument;

        private static ILog s_logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		/// <summary>
		/// The currently open tab / document
		/// </summary>
        public OpenDocumentInfo SelectedDocument
        {
            get { return m_selectedDocument; }
        }

		/// <summary>
		/// The error handlers for the document manager.
		/// </summary>
		public IErrorHandler ErrorHandler
		{
			get;
			protected set;
		}

		public void HandlerError(string message, Exception ex)
		{
			if (ErrorHandler != null)
			{
				ErrorHandler.HandlerError(message, ex, s_logger);
			}
		}

		/// <summary>
		/// Adds an assembly to be searched when AddInitSteps is called to
		/// find all document editors, readers and writers.
		/// </summary>
		/// <param name="assembly"></param>
        public virtual void AddFactoryAssembly(Assembly assembly)
        {
            m_docReaderFactory.Assemblies.Add(assembly);
            m_docWriterFactory.Assemblies.Add(assembly);
            m_docEditorFactory.Assemblies.Add(assembly);
        }

		/// <summary>
		/// Adds task steps to a task that will initialize the document manager.
		/// </summary>
		/// <param name="task"></param>
        public virtual void AddInitSteps(Task task)
        {
            task.AddStep("Loading Readers", () => { m_docReaderFactory.Init(); });
            task.AddStep("Loading Writers", () => { m_docWriterFactory.Init(); });
            task.AddStep("Loading Editors", () => { m_docEditorFactory.Init(); });

            task.AddStep(new TaskStep()
            {
                StatusText = "Setting Up Controls",
                UIThreadPreWork = () =>
                {
                    m_tabViewContext = new TabControlContext() { TabItems = m_documents };
                },
                StepAction = () =>
                {
                    m_closeTabCommand = new DelegateCommand(OnTabClose);
                    this.SelectedDocumentChanged += OnSelectedDocumentChanged;
                }
            });
        }

		/// <summary>
		/// Opens an open file dialog, if the user picks a file, attempt to open it from its path
		/// </summary>
        public virtual void OpenFileDialog()
        {
            OpenFileDialog dialog = new OpenFileDialog();

            if (dialog.ShowDialog() == DialogResult.OK)
            {
                OpenManagedFile(dialog.FileName);
            }
        }

		/// <summary>
		/// Opens a managed document from a file path. Throws an exception if there is no reader,
		/// or no editor for the extension/object type.
		/// </summary>
		/// <param name="filePath"></param>
        public virtual void OpenManagedFile(String filePath)
        {
            try
            {
				OpenDocumentInfo existing = m_documents.FirstOrDefault<OpenDocumentInfo>(doc => doc.Document.FilePath == filePath);
				if (existing != null)
				{
					existing.IsSelected = true;
					return;
				}

                string extension = Path.GetExtension(filePath);
                IDocumentReader reader = m_docReaderFactory.GetReader(extension);
                if (reader == null)
                {
                    s_logger.Debug("No reader found for extension");
                    throw new IOException("No reader found for extension");
                }

				DocumentTask openTask = new DocumentTask() { WindowTitle = "Opening Document" };
				openTask.AddStep("Reading File Data", () =>
					{
						using (FileStream stream = File.Open(filePath, FileMode.Open))
						{
							Document doc = reader.ReadDocument(stream, extension);
							openTask.WorkingDocument = doc;
							stream.Close();
						}
					});
				m_taskManager.RunSynchronousTask(true, openTask);
				Document document = openTask.WorkingDocument;
                document.FilePath = filePath;
                document.FileName = Path.GetFileName(filePath);
                if (document.DocumentData is IEditable) ((IEditable)document.DocumentData).Loaded = true;
                    
                AddManagedDocument(document);
                
            }
            catch (Exception e)
            {
				HandlerError("Error opening file.", e);
            }
        }

		/// <summary>
		/// Adds a document to be managed. If the document is already being managed, this will open the tab.
		/// Throws an exception if an editor can't be found for the data type.
		/// </summary>
		/// <param name="document"></param>
        public virtual void AddManagedDocument(Document document)
        {
            OpenDocumentInfo existing = m_documents.FirstOrDefault<OpenDocumentInfo>(doc => doc.Document.FilePath == document.FilePath);
            if (existing != null)
            {
                existing.IsSelected = true;
                return;
            }

            IDocumentEditor editor = m_docEditorFactory.GetDocumentEditor(document.DocumentData.GetType());
            if (editor == null)
            {
                s_logger.Debug("No editor found for doument data type " + document.DocumentData.GetType());
                throw new InvalidDataException("No editor found for doument data type " + document.DocumentData.GetType());
            }

            IDocumentWriter writer = m_docWriterFactory.GetDocumentWriter(document.DocumentData.GetType());
            editor.Document = document;

            OpenDocumentInfo docInfo = new OpenDocumentInfo() { Document = document, Editor = editor, CloseCommand = m_closeTabCommand, Writer = writer };

            m_documents.Add(docInfo);

            docInfo.PropertyChanged += OnDocumentPropertyChanged;
            docInfo.IsSelected = true;

            if (DocumentAdded != null)
            {
                DocumentAdded(this, new DocumentEventArgs(m_documents.Last()));
            }
        }

		/// <summary>
		/// Saves the currently selected document. If there was no writer found for the document, this will throw an exception.
		/// </summary>
        public virtual void SaveSelectedDocument()
        {
            try
            {
                OpenDocumentInfo doc = SelectedDocument;
                if (doc != null)
                {
                    SaveOpenDocument(doc);
                }
                
            }
            catch (Exception e)
            {
				HandlerError("Error saving selected document", e);
            }
        }


        protected virtual void SaveOpenDocument(OpenDocumentInfo doc)
        {
            if (doc.Writer == null)
            {
                s_logger.Debug("There is no writer available for this type of document.");
                throw new InvalidOperationException("There is no writer available for this type of document.");
            }

            if (doc.Editor.OnSaving() == false) return;

            using (FileStream file = File.Open(doc.Document.FilePath, FileMode.Create))
            {
                doc.Writer.WriteDocument(file, doc.Document);
                file.Close();
            }

            doc.HasUnsavedChanges = false;
            if (DocumentSaved != null)
            {
                DocumentSaved(this, new DocumentEventArgs(doc));
            }
        }

        protected virtual void OnDocumentPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "IsSelected")
            {
                //find new selected
                OpenDocumentInfo doc = m_documents.FirstOrDefault<OpenDocumentInfo>(d => d.IsSelected);
                if (doc != null && SelectedDocumentChanged != null)
                {
                    SelectedDocumentChanged(this, new DocumentEventArgs(doc));
                }
            }
        }

		/// <summary>
		/// Loads document data then returns it without adding it to the managed list.
		/// </summary>
		/// <param name="filePath"></param>
		/// <returns></returns>
        public virtual Document LoadUnmanagedDocument(string filePath)
        {
            string extension = Path.GetExtension(filePath);
            IDocumentReader reader = m_docReaderFactory.GetReader(extension);
            if (reader == null)
            {
                s_logger.Debug("No reader found for extension");
                throw new IOException("No reader found for extension");
            }

            using (FileStream stream = File.Open(filePath, FileMode.Open))
            {
                Document document = reader.ReadDocument(stream, extension);
                document.FilePath = filePath;
                document.FileName = Path.GetFileName(filePath);
                stream.Close();
                return document;
            }
        }

		/// <summary>
		/// Saves an unmanged document. Throws an exception if there is no writer available for the data type.
		/// </summary>
		/// <param name="document"></param>
        public virtual void SaveUnmanagedDocument(Document document)
        {
            IDocumentWriter writer = m_docWriterFactory.GetDocumentWriter(document.DocumentData.GetType());
            if (writer == null)
            {
                s_logger.Debug("No writer found for doument data type " + document.DocumentData.GetType());
                throw new InvalidDataException("No writer found for doument data type " + document.DocumentData.GetType());
            }

            using (FileStream stream = File.Open(document.FilePath, FileMode.Create))
            {
                writer.WriteDocument(stream, document);
                stream.Close();
            }
        }

		/// <summary>
		/// Closes a document by finding a managed document with the file path. If there is unsaved changes,
		/// a box will prompt the user if they want to continue
		/// </summary>
		/// <param name="filePath"></param>
		public virtual void CloseManagedDocument(string filePath)
		{
			OpenDocumentInfo existing = m_documents.FirstOrDefault<OpenDocumentInfo>(doc => doc.Document.FilePath == filePath);
			if (existing != null)
			{
				try
				{
					CloseManagedDocument(existing, false);
				}
				catch (Exception e)
				{
					HandlerError("Error Closing Tab.", e);
				}
			}
		}

        protected virtual void OnTabClose(object tabitem)
        {
            try
            {
                OpenDocumentInfo doc = (OpenDocumentInfo)tabitem;
				CloseManagedDocument(doc, false);
            }
            catch (Exception e)
            {
				HandlerError("Error Closing Tab.", e);
            }
        }

		/// <summary>
		/// Attempts to close each managed document. Documents with unsaved changes will have the user prompted.
		/// </summary>
		/// <returns>True if all documents saved.</returns>
		public virtual bool CloseAllManagedDocuments()
		{
			bool ret = true;
			int closeIndex = 0;
			int count = m_documents.Count;
			for (int i = 0; i < count; i++)
			{
				if (!CloseManagedDocument(m_documents[closeIndex], false))
				{
					closeIndex++;
					ret = false;
				}
			}
			return ret;
		}

		protected virtual bool CloseManagedDocument(OpenDocumentInfo doc, bool overrideChanges)
		{
			if (!overrideChanges && doc.HasUnsavedChanges)
			{
				if (MessageBox.Show(String.Format("Document {0} has unsaved changes, close without saving?", doc.Document.FileName), "Close Document", MessageBoxButtons.YesNo) == DialogResult.No)
				{
					return false;
				}
			}

			m_documents.Remove(doc);
			if (m_documents.Count == 0)
			{
				SelectedDocumentChanged(this, new DocumentEventArgs(null));
			}
			doc.PropertyChanged -= OnDocumentPropertyChanged;

			if (DocumentClosed != null)
			{
				DocumentClosed(this, new DocumentEventArgs(doc));
			}
			return true;
		}

        protected virtual void OnSelectedDocumentChanged(object sender, DocumentEventArgs e)
        {
            this.m_selectedDocument = e.DocumentInfo;
            if (e.DocumentInfo != null)
            {
                e.DocumentInfo.Editor.OnSelected();
            }
        }

		/// <summary>
		/// An object to use as the data context for a document tab view.
		/// </summary>
        public virtual object TabViewContext
        {
            get { return m_tabViewContext; }
        }

        public event DocumentEventHandler DocumentAdded;

        public event DocumentEventHandler DocumentClosed;

        public event DocumentEventHandler DocumentSaved;

        public event DocumentEventHandler SelectedDocumentChanged;
    }
}
